package com.ndood.reconciliation.domain.recon.service;

import java.io.File;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayDataDataserviceBillDownloadurlQueryRequest;
import com.alipay.api.response.AlipayDataDataserviceBillDownloadurlQueryResponse;
import com.ndood.api.app.properties.ApiProperties;
import com.ndood.api.base.constaints.ApiCode;
import com.ndood.api.base.exception.ApiException;
import com.ndood.api.domain.record.receive.v1.entity.dto.ReconAppDto;
import com.ndood.api.domain.record.receive.v1.service.RecoRecMockService;
import com.ndood.api.domain.record.receive.v1.service.RecoRecPayService;
import com.ndood.common.base.constaints.CommonConstaints;
import com.ndood.common.base.util.BigDecimalUtil;
import com.ndood.common.base.util.DateUtil;
import com.ndood.reconciliation.base.constaints.ReconConstaints;
import com.ndood.reconciliation.base.dao.recon.ExtendReconScratchDao;
import com.ndood.reconciliation.base.dao.recon.ExtendReconTradeRecordDao;
import com.ndood.reconciliation.base.util.CsvUtil;
import com.ndood.reconciliation.base.util.FileUtil;
import com.ndood.reconciliation.domain.recon.entity.dto.ReconBatchDto;
import com.ndood.reconciliation.domain.recon.entity.dto.ReconInfoDto;
import com.ndood.reconciliation.domain.recon.entity.dto.ReconMistakeDto;
import com.ndood.reconciliation.domain.recon.entity.dto.ReconScratchDto;
import com.ndood.reconciliation.domain.recon.entity.dto.ReconTradeRecordDto;

import lombok.extern.slf4j.Slf4j;

/**
 * 支付宝线下对账服务
 */
@Service
@Slf4j
public class ReconAlipayOfflineService {

	@Autowired
	private ApiProperties apiProperties;

	@Autowired
	private ReconCommonService commonService;

	@Autowired
	private RecoRecPayService recoRecPayService;

	@Autowired
	private ExtendReconTradeRecordDao tradeRecordDao;

	@Autowired
	private RecoRecMockService recoRecMockService;
	
	@Autowired
	private ReconCommonService reconCommonService;
	
	@Autowired
	private ExtendReconScratchDao reconScratchDao;
	
	/**
	 * 1 支付宝对账
	 */
	public void doWithReconciliation(ReconInfoDto reconInfo) throws Exception {

		// Step1: 判断对账批次是否存在，不存在则新建，可重构成insert on duplicate update
		ReconBatchDto batch = commonService.getReconBatch(reconInfo.getDate(), reconInfo.getPayway().getCode());
		if (batch != null) {
			if (ReconConstaints.RECON_BATCH_STATUS_OK.equals(batch.getStatus())) {
				log.info("该批次已完成对账:" + reconInfo.getDate() + "-" + reconInfo.getPayway().getName());
				return;
			}
		}
		if (batch == null) {
			log.info("创建新的对账批次:" + reconInfo.getDate() + "-" + reconInfo.getPayway().getName());
			commonService.initReconBatch(reconInfo.getDate(), reconInfo.getPayway().getCode());
		}
		
		// Step2: 获取所有官方支付宝app
		List<ReconAppDto> appList = recoRecPayService.getLastDayRecAppList();
		if (appList.isEmpty()) {
			log.info("没有需要对账的应用信息:" + reconInfo.getDate() + "-" + reconInfo.getPayway().getName());
			return;
		}
		
		// Step3: 进行对账
		List<ReconTradeRecordDto> remoteAllList = new ArrayList<>();
		for (ReconAppDto app : appList) {
			try {
				// 1: 下载对账单并解析
				// downloadReconFile(reconInfo, app);
				
				// 2: 解析对账文件
				// List<ReconTradeRecordDto> list = parseReconFile(reconInfo, app);

				// 3: 模拟对账数据
				List<ReconTradeRecordDto> mockList = mockReconData(reconInfo, app);
				remoteAllList.addAll(mockList);

			} catch (Exception e) {
				log.error(e.getMessage(), e);
			}
		}
		// Step4: 对账
		processReconciliation(reconInfo, remoteAllList, batch);
	}

	/**
	 * 1 下载支付宝对账单 https://docs.open.alipay.com/204/106262/
	 */
	private void downloadReconFile(ReconInfoDto reconInfo, Map<String, String> appMap) throws Exception {

		// Step1: 获取下载链接
		AlipayClient alipayClient = new DefaultAlipayClient(apiProperties.getPay().getAlipayGateway(),
				appMap.get("appId"), appMap.get("mchPrivateRsa2Secret"), "json", "UTF-8",
				appMap.get("alipayPublicRsa2Secret"), "RSA2");
		AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
		request.setBizContent("{\"bill_type\":\"trade\",\"bill_date\":\"" + reconInfo.getDate() + "\"}");
		AlipayDataDataserviceBillDownloadurlQueryResponse response = null;
		try {
			response = alipayClient.execute(request);
			log.debug(response.getBillDownloadUrl());
		} catch (AlipayApiException e) {
			log.error(e.getMessage(), e);
			throw new ApiException(ApiCode.ERR_SERVER, "下载对账文件失败");
		}
		if (!response.isSuccess()) {
			throw new ApiException(ApiCode.ERR_SERVER, "下载对账文件失败");
		}
		String url = response.getBillDownloadUrl();

		// Step2: 下载文件并解压缩
		try {
			String filePath = apiProperties.getRecon().getReconZipDir();
			String newZip = filePath + new Date().getTime() + ".zip";
			FileUtil.downloadNet(url, newZip);
			FileUtil.unZip(newZip, apiProperties.getRecon().getReconFileDir() + File.separator + reconInfo.getDate());
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			throw new ApiException(ApiCode.ERR_SERVER, "解压对账文件失败");
		}

	}

	/**
	 * 2 解析对账单 20881021702160310156_20190328_业务明细.csv
	 * https://www.cnblogs.com/zgghb/p/5972972.html
	 */
	private List<HashMap<String, String>> parseReconFile(ReconInfoDto reconInfo, Map<String, String> appMap)
			throws Exception {
		// Step1: 获取csv文件列表
		File dir = new File(apiProperties.getRecon().getReconFileDir() + File.separator + reconInfo.getDate());
		if (!dir.exists()) {
			throw new ApiException(ApiCode.ERR_SERVER,
					"对账文件下载失败" + reconInfo.getDate() + reconInfo.getPayway().getName());
		}
		File[] fs = new File(apiProperties.getRecon().getReconFileDir() + File.separator + reconInfo.getDate())
				.listFiles();
		String mchId = appMap.get("mchId");
		String date = DateUtil.translateTime(reconInfo.getDate(), "yyyy-MM-dd", "yyyyMMdd");
		String prefix = mchId + "_" + date;

		// Step2: 解析对账文件
		List<HashMap<String, String>> orderListMap = new ArrayList<>();
		for (File file : fs) {
			if (file.getAbsolutePath().contains("汇总")) {
				continue;
			}
			if (file.getAbsolutePath().contains(prefix)) {
				orderListMap = CsvUtil.parseOrderList(file.getAbsolutePath());
			}
		}
		Assert.notEmpty(orderListMap, "对账文件解析失败:" + reconInfo.getDate() + "-" + reconInfo.getPayway().getName());
		return orderListMap;
	}

	/**
	 * 3 模拟对账数据
	 */
	private List<ReconTradeRecordDto> mockReconData(ReconInfoDto reconInfo, ReconAppDto app) throws Exception {
		
		// Step1: 模拟出收款撤销，退款撤销的订单，用来生成长款数据
		Date startTime = DateUtil.parseDate(reconInfo.getDate(), "yyyyMMdd");
		Date endTime = DateUtil.getAfterDate(startTime, 1, 0, 0, 0);
		String appId = app.getAppId();
		
		// 如果是平台二清支付，则伪造失败数据，用来模拟长款数据
		if(apiProperties.getPay().getAlipayAppId().equals(appId)) {
			recoRecMockService.mockAPlatformOfflineOfficialAlipayPayCancelTrade(appId);
			recoRecMockService.mockAPlatformOfflineOfficialAlipayRefundFailedTrade(appId);
		}
		
		Integer payway = reconInfo.getPayway().getCode();
		List<ReconTradeRecordDto> tradeRecordList = tradeRecordDao
				.extendFindAllTradeRecordListByTimeZoneAndPaywayAndAppid(startTime, endTime, payway, appId);

		// Step2: 统计正常订单数，失败订单数
		List<ReconTradeRecordDto> payOkList = new ArrayList<>();
		List<ReconTradeRecordDto> payNotList = new ArrayList<>();
		List<ReconTradeRecordDto> refundOkList = new ArrayList<>();
		List<ReconTradeRecordDto> refundNotList = new ArrayList<>();
		for (ReconTradeRecordDto tradeRecord : tradeRecordList) {
			if (tradeRecord.getTradeType() == CommonConstaints.TRADE_TYPE_REC_PAY) {
				if(tradeRecord.getTradeStatus() == CommonConstaints.REC_TRADE_STATUS_SUCCESS) {
					payNotList.add(tradeRecord);
				} else {
					payOkList.add(tradeRecord);
				}
			}
			if (tradeRecord.getTradeType() == CommonConstaints.TRADE_TYPE_REC_REFUND) {
				if(tradeRecord.getTradeStatus() == CommonConstaints.REC_TRADE_STATUS_SUCCESS) {
					refundNotList.add(tradeRecord);
				} else {
					refundOkList.add(tradeRecord);
				}
			}
		}

		List<ReconTradeRecordDto> mockList = new ArrayList<>();

		// Step3: 模拟正常数据
		for(int i = 0; i < payOkList.size(); i++) {
			ReconTradeRecordDto temp = new ReconTradeRecordDto();
			BeanUtils.copyProperties(temp, payOkList.get(i));
			
			// Step4: 模拟金额不一致数据：本地已支付，支付渠道已支付，金额不一致
			if(i == 0) {
				temp.setRealAmount(BigDecimalUtil.add(temp.getRealAmount(), new BigDecimal(Math.random())));
			}
			mockList.add(temp);
		}

		// Step5: 模拟支付长款数据：本地未支付或无记录，支付渠道已支付
		// 思路：将本地失败收款变成mock成功
		if (payNotList.size() > 0) {
			ReconTradeRecordDto temp = new ReconTradeRecordDto();
			BeanUtils.copyProperties(temp, payNotList.get(0));
			temp.setRealAmount(temp.getAmount());
			temp.setTradeStatus(CommonConstaints.REC_TRADE_STATUS_SUCCESS);
			mockList.add(temp);
		}

		// Step6: 模拟短款数据：本地已支付，支付渠道未支付
		// 思路：不生成mock数据即可

		// Step7: 模拟退款成功数据
		for (int i = 0; i< refundOkList.size(); i++) {
			ReconTradeRecordDto temp = new ReconTradeRecordDto();
			BeanUtils.copyProperties(temp, refundOkList.get(i));
			
			// Step8: 模拟退款金额不一致数据
			temp.setRealAmount(BigDecimalUtil.sub(temp.getRealAmount(), new BigDecimal(Math.random())));
			mockList.add(temp);
		}
		
		// Step9: 模拟本地未退款，支付渠道已退款长款数据
		// 思路：将本地失败退款变成mock成功
		if (refundNotList.size() > 0) {
			ReconTradeRecordDto temp = new ReconTradeRecordDto();
			BeanUtils.copyProperties(temp, refundNotList.get(0));
			temp.setTradeStatus(CommonConstaints.REC_TRADE_STATUS_SUCCESS);
			mockList.add(temp);
		}

		// Step10: 模拟本地已退款，支付渠道未退款
		// 思路：不生产mock退款数据即可

		return mockList;
	}

	/**
	 * 4 处理对账业务
	 * https://gitee.com/roncoocom/roncoo-pay/blob/master/roncoo-pay-app-reconciliation/src/main/java/com/roncoo/pay/app/reconciliation/biz/ReconciliationCheckBiz.java
	 */
	private void processReconciliation(ReconInfoDto reconInfo, List<ReconTradeRecordDto> remoteAllList, ReconBatchDto batch) throws Exception {
		// Step1 初始化变量
		if(remoteAllList.isEmpty()) {
			log.info("对账文件内容为空，不进行对账处理！");
			return;
		}
		List<ReconMistakeDto> tempMistakeList = new ArrayList<>(); // 差错list，对账差错直接放到差错处理列表，等待平账处理
		List<ReconScratchDto> tempInsertScratchList = new ArrayList<>(); // 需要放入缓冲池中平台短款list： 本地有远程没有，需要把本地放入缓冲池，明天远程有了匹配上再移除
		List<ReconScratchDto> tempRemoveScratchList = new ArrayList<>(); // 需要从缓冲池中移除的数据。当缓冲池匹配上远程了就移除

		// Step2: 查询数据库中的记录准备对账 
		Date startTime = DateUtil.parseDate(batch.getReconDay(),"yyyyMMdd");
		Date endTime = DateUtil.getAfterDate(startTime, 1, 0, 0, 0);
		List<ReconTradeRecordDto> localAllTradeList = tradeRecordDao
				.extendFindAllTradeRecordListByTimeZoneAndPayway(startTime, endTime, batch.getPayWay()); // 查询平台date, 成功的交易
		List<ReconTradeRecordDto> localSuccessList = tradeRecordDao
				.extendFindSuccessTradeRecordListByTimeZoneAndPayway(startTime, endTime, batch.getPayWay()); // 查询平台date, 所有的交易（创建，退款，撤销...）
		List<ReconScratchDto> scratchList = reconScratchDao.extendFindScratchListByTimeZoneAndPayway(startTime, endTime, batch.getPayWay());// 查询平台缓冲池数据
		
		// Step3: 开始以平台的数据为准对账,平台长款记入缓冲池
		log.info("===>开始以平台的数据为准对账,平台短款记入缓冲池");
		baseOnLocalRecociliation(localSuccessList, remoteAllList, tempInsertScratchList, batch, tempMistakeList);
		log.info("===>结束以平台的数据为准对账");
		
		// Step4: 开始以银行通道的数据为准对账
		log.info("<===开始以银行通道的数据为准对账");
		baseOnRemoteRecociliation(localAllTradeList, remoteAllList, scratchList, batch, tempMistakeList, tempRemoveScratchList);
		log.info("<===结束以银行通道的数据为准对账");
		
		// Step5 保存对账结果
		reconCommonService.refreshReconBatchData(batch, tempMistakeList, tempInsertScratchList, tempRemoveScratchList);
	}

	/**
	 * 5 以本地为主进行对账
	 */
	private void baseOnLocalRecociliation(List<ReconTradeRecordDto> localSuccessList, List<ReconTradeRecordDto> remoteAllList, List<ReconScratchDto> scratchList, ReconBatchDto batch, List<ReconMistakeDto> mistakeList) throws Exception {
		BigDecimal tradeAmount = BigDecimal.ZERO;
		Integer tradeCount = 0;
		Integer mistakeCount = 0;
		for (ReconTradeRecordDto localSuccess : localSuccessList) {
			boolean isFind = false;
			// 金额不一致
			for (ReconTradeRecordDto remote : remoteAllList) {
				tradeAmount = BigDecimalUtil.add(tradeAmount, localSuccess.getAmount());
				tradeCount ++;
				if(remote.getTradeOrderNo().equals(localSuccess.getTradeOrderNo())) {
					if(remote.getTradeStatus() == CommonConstaints.REC_TRADE_STATUS_SUCCESS) {
						isFind = true;
						// 匹配平台金额
						// 平台金额多
						if(localSuccess.getRealAmount().compareTo(remote.getRealAmount()) == 1) {
							ReconMistakeDto mistake = createMistake(null, localSuccess, remote, ReconConstaints.PLATFORM_OVER_CASH_MISMATCH);
							mistakeList.add(mistake);
							mistakeCount ++;
							break;
						}
						// 平台金额少
						if(localSuccess.getRealAmount().compareTo(remote.getRealAmount()) == -1) {
							ReconMistakeDto mistake = createMistake(null, localSuccess, remote, ReconConstaints.PLATFORM_SHORT_CASH_MISMATCH);
							mistakeList.add(mistake);
							mistakeCount ++;
							break;
						}
					}
				}
			}
			// 对账时间窗缓冲池
			if(!isFind) {
				ReconScratchDto temp = new ReconScratchDto();
				BeanUtils.copyProperties(temp, localSuccess);
				scratchList.add(temp);
			}
		}
		batch.setTradeAmount(tradeAmount);
		batch.setTradeCount(tradeCount);
		batch.setMistakeCount(mistakeCount);
	}

	private ReconMistakeDto createMistake(Object object, ReconTradeRecordDto localSuccess, ReconTradeRecordDto remote,
			String platformOverCashMismatch) {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * 6 以三方平台为主进行对账
	 */
	private void baseOnRemoteRecociliation(List<ReconTradeRecordDto> localAllTradeList, List<ReconTradeRecordDto> remoteTradeList, List<ReconScratchDto> scratchList, ReconBatchDto batch, List<ReconMistakeDto> mistakeList, List<ReconScratchDto> removeScratchList) {
		
	}

}
